using System;
using System.Collections;
using System.Reflection;

namespace NUtil
{
    [Serializable]
    public class EqualsBuilder
    {
        private IList comparedFields = new ArrayList();
        private string _message = "";
        private IList ignoredTypes = new ArrayList();
        private IList ignoredfields = new ArrayList();
        private string _detailedMessage = "";

        public string FailureReason
        {
            get { return _message; }
        }

        public string DetailedFailureReason
        {
            get { return _detailedMessage; }
        }

        public bool AreEqual(object first, object other)
        {
            if (first == other)
            {
                return true;
            }
            if (first == null || other == null)
            {
                return false;
            }

            if (first.GetType() != other.GetType())
            {
                return false;
            }

            Type typeOfClass = first.GetType();
            return areEqual(first, other, typeOfClass);
        }

        private bool areEqual(object first, object other, Type typeOfClass)
        {
            if (ignoredTypes.Contains(typeOfClass))
            {
                return true;
            }

            if (typeOfClass == typeof(string) || typeOfClass.IsValueType || typeOfClass.IsPrimitive)
            {
                return first.Equals(other);
            }

            if (typeof(IList).IsAssignableFrom(typeOfClass))
            {
                return listEquals((IList)first, (IList)other);
            }
            if (!typeOfClass.BaseType.Equals(typeof(Object)))
            {
                if (!areEqual(first, other, typeOfClass.BaseType))
                {
                    return false;
                }
            }
            return compareFields(first, other, typeOfClass);
        }

        private bool compareFields(object first, object other, Type typeOfClass)
        {
            BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            FieldInfo[] fields = typeOfClass.GetFields(bindingFlags);
            foreach (FieldInfo field in fields)
            {
                if (comparedAlready(field))
                {
                    return true;
                }
                comparedFields.Add(field);
                if (ignoredfields.Contains(field.Name))
                    continue;
                if (typeof(IList).IsAssignableFrom(field.FieldType))
                {
                    if (!listEquals((IList)field.GetValue(first), (IList)field.GetValue(other)))
                    {
                        appendError("List " + field.Name + " were different");
                        return false;
                    }
                }
                else if (!AreEqual(field.GetValue(first), field.GetValue(other)))
                {
                    appendError("Field " + field.Name + " were different - values are " + field.GetValue(first) + " , " + field.GetValue(other));
                    return false;
                }
            }
            return true;
        }

        private void appendError(string error)
        {
            _message = error;
            _detailedMessage = error + ">>" + _detailedMessage;
        }

        private bool comparedAlready(FieldInfo field)
        {
            return comparedFields.Contains(field);
        }

        private bool listEquals(IList first, IList other)
        {
            return ListReflectionEquals(first, other);
        }

        /// <summary>
        /// Tests for equality between two generic lists of the same type. This is being achieved by calling Equals on corresponding items.
        /// </summary>
        /// <param name="list1"></param>
        /// <param name="list2"></param>
        /// <returns></returns>
        public bool ListReflectionEquals(IList list1, IList list2)
        {
            if (list1 == null)
            {
                if (list2 == null)
                {
                    return true;
                }
                return false;
            }

            if (list1.Count != list2.Count)
            {
                return false;
            }
            else
            {
                for (int i = 0; i < list1.Count; i++)
                {
                    Object obj1 = list1[i];
                    if (!find(list2, obj1))
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private bool find(IList list, object objToBeFound)
        {
            foreach (object obj in list)
            {
                if (AreEqual(obj, objToBeFound))
                {
                    return true;
                }
            }
            return false;
        }

        public EqualsBuilder IgnoreFieldsByType(Type type)
        {
            ignoredTypes.Add(type);
            return this;
        }

        public EqualsBuilder IgnoreFieldByName(params string[] fieldNamesToIgnore)
        {
            foreach (string field in fieldNamesToIgnore)
            {
                ignoredfields.Add(field);
            }
            return this;
        }
    }
}